package ch.cm.tinkerforge;

import java.io.IOException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.tinkerforge.AlreadyConnectedException;
import com.tinkerforge.Device;
import com.tinkerforge.IPConnection;
import com.tinkerforge.IPConnection.ConnectedListener;
import com.tinkerforge.IPConnection.DisconnectedListener;
import com.tinkerforge.IPConnection.EnumerateListener;
import com.tinkerforge.NotConnectedException;
import com.tinkerforge.TinkerforgeException;

/**
 * Einelnes, spezifisches Device.
 * 
 * @author Christian, Reto
 * 
 */
public abstract class AbstractTinkerDevice {
    public static final int DEFAULT_PORT = 4223;
    private final String host;
    private final int port;

    private final IPConnection ipConnection;
    private final Map<String, Device> deviceMap;

    @SuppressWarnings("unused")
    private final IPConnectionHandler ipConnectionHandler;
    @SuppressWarnings("unused")
    private final DeviceEnumerationHandler deviceEnumerationHandler;

    /**
     * 'localhost' oder spezifische IP 
     * 
     * @param host
     */
    public AbstractTinkerDevice(String host) {
        this(host, DEFAULT_PORT);
    }

    /**
     * 'localhost' oder spezifische IP 
     * 
     * @param host
     * @param port
     */
    public AbstractTinkerDevice(String host, int port) {
        this.deviceMap = new HashMap<String, Device>();
        this.host = host;
        this.port = port;
        this.ipConnection = new IPConnection();
        this.ipConnectionHandler = new IPConnectionHandler(ipConnection);
        this.deviceEnumerationHandler = new DeviceEnumerationHandler(ipConnection);
    }

    /**
     * Connects to the real Tinkerforge-Stack. It then requests the connected
     * {@link Device}s. Therefore it waits for some time (3-seconds)
     * 
     * @throws UnknownHostException
     * @throws IOException
     */
    public void connect() throws UnknownHostException, IOException {
        try {
            this.ipConnection.connect(host, port);
            Thread.sleep(3000);
        } catch (AlreadyConnectedException e) {
            // So what
        } catch (InterruptedException e) {
            // OK, we go on
        }
    }

    /**
     * Disconnects from a real Tinkerforge-Stack.
     */
    public void disconnect() {
        try {
            this.ipConnection.disconnect();
        } catch (NotConnectedException e) {
            // So what
        }
    }

    /**
     * Called when the brick has been connected
     */
    public abstract void connected();

    /**
     * Called when the Brick has been disconnected
     */
    public abstract void disconnected();

    /**
     * Called when a new {@link Device} has been found/connected
     * 
     * @param device
     */
    public abstract void deviceConnected(Device device);

    /**
     * Called when a {@link Device} already known to the system has been
     * reconnected. A good strategy is to drop the old device-instance and
     * connect the Listeners to this new instance of the {@link Device}.
     * 
     * @param device
     */
    public abstract void deviceReConnected(Device device);

    /**
     * Called when a {@link Device} already known to the system has been
     * disconnected.
     * 
     * @param device
     */
    public abstract void deviceDisconnected(Device device);

    public boolean isConnected(Device device) {
        try {
            device.getIdentity();
            return true;
        } catch (TinkerforgeException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * Returns the most recent list of all valid connected {@link Device}s.
     * 
     * @return
     */
    public List<Device> getConnectedDeviceList() {
        return new ArrayList<Device>(deviceMap.values());
    }

    private class IPConnectionHandler implements ConnectedListener, DisconnectedListener {
        private IPConnection connection;

        public IPConnectionHandler(IPConnection connection) {
            this.connection = connection;
            connection.addConnectedListener(this);
            connection.addDisconnectedListener(this);
        }

        @Override
        public void disconnected(short disconnectReason) {
            AbstractTinkerDevice.this.disconnected();

        }

        @Override
        public void connected(short disconnectReason) {
            try {
                connection.enumerate();
                AbstractTinkerDevice.this.connected();
            } catch (NotConnectedException ex) {
                // Well, this should not happen?!
                // But will treat it gracefully.
                ex.printStackTrace();
            }
        }
    }

    private class DeviceEnumerationHandler implements EnumerateListener {
        private IPConnection connection;

        public DeviceEnumerationHandler(IPConnection connection) {
            this.connection = connection;
            this.connection.addEnumerateListener(this);
        }

        @Override
        public void enumerate(String uid, String connectedUid, char position, short[] hardwareVersion,
                short[] firmwareVersion, int deviceIdentifier, short enumerationType) {

            createDevice(deviceIdentifier, uid);
            if (enumerationType == 0)
                deviceConnected(deviceMap.get(uid));
            if (enumerationType == 1)
                deviceReConnected(deviceMap.get(uid));
            if (enumerationType == 2)
                deviceDisconnected(deviceMap.get(uid));
        }

        private void createDevice(int deviceIdentifier, String uid) {
            if (!deviceMap.containsKey(uid)) {
                try {
                    Device device = (Device) TinkerforgeDevice.getDevice(deviceIdentifier).device
                            .getDeclaredConstructor(String.class, IPConnection.class).newInstance(uid, ipConnection);
                    device.getIdentity();
                    deviceMap.put(uid, device);
                } catch (TinkerforgeException ex) {
                    ex.printStackTrace();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }

            }
        }

    }

}
